package network;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JOptionPane;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

import kpsmart.model.events.Event;
import kpsmart.model.events.Events;
import kpsmart.model.events.Simulation;

/**
 * The ServerController begins listening to the remote server for feedback, in the form 
 * of an updated file (which the GUI will then promptly update) or by writing information
 * to the server so other client's can be made aware of the user making changes
 * 
 * This is threaded so that the network connection does not get choked whilst the window
 * is being drawn/interacted with.
 * 
 * 
 * @author Oliver Behncke 
 * @author Holly Denton 
 * @author Matthew McCarthy
 * @author John Quinlivan
 *
 */
public class ServerController extends Thread {

	/* The socket to listen and perform requests on */
	private Socket server;

	/* This event listener will be passed all new information as it is received
	 * from the server side */
	private ServerEventListener eventListener;

	/* Stream for writing to */
	private OutputStream sendToServer;

	/* Stream for reading from */
	private BufferedInputStream receivedFromServer;

	/* Stores the latest message from the server - must be a "PacketConstant" */
	private byte lastMessageFromServer;

	/* Prevents another method access the streams if a process is already occurring */
	private boolean currentlyWorking;

	/* A list of all events that need to be sent */
	private List<Event> eventList;

	/**
	 * Initialises the server listener, requires "start()" be called to begin
	 * reading/writing to the input/output streams of the remote server
	 * 
	 * @param socket The socket that is presently connected with the server
	 */
	public ServerController(Socket socket) {

		/* Assign server and relevant fields */
		server = socket;
		
		/* Build the event list */
		eventList = new ArrayList<Event>();

		/* Request the streams to which reading/writing is possible */
		try {
			/* Stream that can be written to */
			sendToServer = server.getOutputStream();

			/* Stream that can be read from */
			receivedFromServer = new BufferedInputStream(server.getInputStream());

			/* Ready the state machine */
			lastMessageFromServer = PacketConstants.NULL;

		} catch (Exception e) {
			System.err.println("Error getting I/O stream from server: " + server.getInetAddress() + 
					" on port: " + server.getPort() + "!");
			e.printStackTrace(); 
			killConnection();
		}

		/* Allow modifications */
		currentlyWorking = false;
	}

	@Override
	public void run(){

		/* Await input */
		lastMessageFromServer = PacketConstants.NULL;

		/* Performs whilst still valid */
		while(lastMessageFromServer != PacketConstants.TERMINATED){

			/* Wait for a message from the server */
			listenForInput();

			/* Perform the appropriate output */
			respondToInput();

		}
	}

	/**
	 * Whilst the thread is idle, will constantly wait for input from the server-side. 
	 * 
	 * Blocks until input received
	 */
	private void listenForInput() {
		try {
			/* Attempts to read from the server, blocks until complete */
			byte[] info = new byte[1];
			receivedFromServer.read(info);
			lastMessageFromServer = info[0];
		} catch (Exception e) {
			System.out.println("Error reading input from remote server! (Server is likely to have been stopped)");
			killConnection();
		}
	}

	/**
	 * Sends an appropriate response to the server
	 */
	private void respondToInput() {

		/* Receive a file */
		if(lastMessageFromServer == PacketConstants.SENDING_DATA){
			receiveFile();
			lastMessageFromServer = PacketConstants.NULL;
		}
	}

	/**
	 * Prepares to receive a file (as an array of bytes) from the server, informs that this is ready
	 * before a transfer occurs 
	 */
	private void receiveFile() {

		/* Stop if the thread is busy elsewhere */
		if(currentlyWorking)
			return;

		/* Prevent multiple tasks */
		currentlyWorking = true;

		try {

			/* Inform the server that this client is ready to transfer */
			sendToServer.write(new byte[] { PacketConstants.READY_FOR_DATA_TRANSFER });

			/* Generate a byte array to poll the expected length of the file from */
			byte[] lengthOfFile = new byte[4];

			/* Grab the int (in byte form) */
			receivedFromServer.read(lengthOfFile);

			/* Wrap in a ByteBuffer */
			ByteBuffer bb = ByteBuffer.wrap(lengthOfFile);

			/* Read expected size of file */
			byte[] readFromServer = new byte[bb.getInt()];

			/* Read the whole stream at once */
			receivedFromServer.read(readFromServer);

			/* Creates a new temp file on the disk */
			File fileToWrite = new File("networking/latestVersion.xml");
			/* Delete the old database */
			if(fileToWrite.exists()){
				fileToWrite.delete();
			}
			fileToWrite.createNewFile();

			/* Writes the byte array to the file */
			FileOutputStream writer = new FileOutputStream(fileToWrite);
			writer.write(readFromServer);
			writer.close();

			/* Copy this new version to the running database */
			File f1 = new File("networking/latestVersion.xml");
			File f2 = new File(eventListener.getLogFilePath());

			/* Creates I/O streams */
			InputStream in = new FileInputStream(f1);
			OutputStream out = new FileOutputStream(f2);

			/* Generate a buffer equal to the length of the file */
			byte[] buf = new byte[(int) f1.length()];

			/* Read and write */
			in.read(buf);
			out.write(buf);

			/* Close all */
			in.close();
			out.close();

			/* Update the runtime state */
			eventListener.changeOnUpdatedDatabase();
		} catch (Exception e) {
			System.out.println("Error receiving file from the server!");
			killConnection();
		}

		/* Allow another task to occur */
		currentlyWorking = false;

	}

	/**
	 * In the event of a malformed database being interpreted, ask for it to be passed again
	 */
	public void requestDatabase() {
	
		try{
			/* Inform the connected pair this needs a new database */
			sendToServer.write(new byte[] { PacketConstants.REQUEST_DATABASE });
		} catch(Exception e){
			System.out.println("Error requesting a new database!");
			killConnection();
		}
	}

	/**
	 * Attempts to pass the event to the remote server, this is done by:
	 * 
	 * 		- Writing event to byte array
	 * 		- Inform server of intention
	 * 		- Await a confirmation
	 * 		- Send byte array to server
	 * 		- Receive confirmation
	 * 
	 * Will kill any invalid connection by invoking the end of this thread with error.
	 * 
	 * Blocks input from server whilst processing 
	 * 
	 * @param e Event to send to the server
	 */
	public void sendEvent(Event e) {

		/* Stop if the thread is busy elsewhere - don't just forget the event though */
		if(currentlyWorking){
			eventList.add(e);
			return;
		}
		
		/* Copy the event list - no more additions allowed */
		List<Event> copyList = new ArrayList<Event>();
		for(Event ev :eventList)
			copyList.add(ev);
		eventList.clear();

		/* Prevent multiple tasks */
		currentlyWorking = true;

		try{ 

			/* Inform the connected pair this is going to send some data */
			sendToServer.write(new byte[] { PacketConstants.SENDING_DATA });

			/* Allows a laggy packet */
			Thread.sleep(100);

			/* Create a byte array from the event */
			JAXBContext context = JAXBContext.newInstance(Events.getJAXBContext());
			Simulation simulation = new Simulation();
			copyList.add(e);
			simulation.setPriceOrCostOrMail(copyList);
			Marshaller marshaller = context.createMarshaller();
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
			ByteArrayOutputStream stream = new ByteArrayOutputStream();
			marshaller.marshal(simulation, stream);

			/* Tell the server how long the event is */
			ByteBuffer arrayLength = ByteBuffer.allocate(4);
			arrayLength.putInt(stream.toByteArray().length);
			sendToServer.write(arrayLength.array());

			/* Allows a laggy packet */
			Thread.sleep(100);

			/* Send the package */
			sendToServer.write(stream.toByteArray());

		} catch (Exception error) {
			error.printStackTrace();
			System.out.println("Error sending event " + e);
			killConnection();
		}

		/* Allow another task to occur */
		currentlyWorking = false;
	}

	/**
	 * Assigns the new listener to the field and begins passing it events as received.
	 * 
	 * @param listener The event listener that wishes to hear updates from the server
	 */
	public void setServerEventListener(ServerEventListener listener){
		eventListener = listener;
	}

	/**
	 * Method that terminates the connection with the remote peer. Whether forced or not 
	 */
	private void killConnection() {

		/* Prevent multiple tasks */
		currentlyWorking = true;

		if(lastMessageFromServer != PacketConstants.TERMINATED){
			/* Inform the UI */
			JOptionPane.showMessageDialog(null, "\nConnection with the server has been terminated!");

			/* Close all streams */
			try{
				receivedFromServer.close();
				sendToServer.close();
				server.close();
			} catch(Exception e){
				System.err.println("Termination with the server is causing issues");
			}

			/* Null and garbage collect */
			server = null;
			sendToServer = null;
			receivedFromServer = null;
			eventList = null;
			lastMessageFromServer = PacketConstants.TERMINATED;
			System.gc();
		}
	}	

}
